Le code source de cet exemple peut être téléchargé ici.
La boucle REPL (Read Eval Print Loop) est une méthode d'interaction avec un programme informatique. C'est la forme la plus basique, la plus facile à mettre en œuvre mais également la plus ancienne (dès le début des années 60). Elle est utilisée principalement dans un terminal (ou console) mais on peut également la retrouver dans des interfaces graphiques.
Elle se compose de 3 séquences répétées à l'infinie jusqu'à la sortie du programme.
L'utilisateur tape une série d'instructions dans une invite de console. Une fois validée, les instructions sont lues par le programme.
Les instructions sont évaluées par le programme et les différentes opérations demandées par l'utilisateur sont effectuées.
Le résultat des opérations est affiché sous une forme compréhensible par l'utilisateur. Le programme peut également afficher des messages d'erreurs dans le cas d'instructions erronées.
Voyons maintenant comment implémenter une boucle REPL simple avec Haskell.
Pour cet exemple nous allons réaliser un programme simple permettant d'obtenir différentes informations telles que :
La liste des fichiers du répertoire courant.
L'heure du système.
La date du système.
La taille et la date de modification d'un fichier.
Pour la partie lecture, on utilise une monade IO
très simple qui :
Affiche une invite de commande.
Attends et récupère une chaîne de caractère renseignée par l'utilisateur via la fonction getLine
.
Retourne la chaîne de caractère.
replRead :: IO String
replRead = do
putStr "ma commande>"
com <- getLine
putStrLn com
return com
L'évaluation se fait en analysant la chaîne de caractère de la commande et en exécutant les actions correspondantes.
Pour lister les fichiers d'un répertoire quelconque, on utilise la fonction getDirectoryContents
du module System.Directory
Afin de supprimer les résultats .
et ..
, on effectue un filtrage de ces éléments avec filter
couplé avec notElem
. Et pour organiser les résultats de façon cohérente, on trie les résultats avec sort
.
replEval :: String -> IO [String]
replEval com@(':' : 'l' : 's' : ' ' : dir ) = do
content <- getDirectoryContents dir
let filteredContent = sort $ filter (\f -> notElem f [".", ".."]) content
return filteredContent
Pour lister les fichiers du répertoire courant, on utilise d'abord getCurrentDirectory
qui retourne le répertoire courant et que l'on repasse comme argument à getDirectoryContents
replEval com@(':' : 'l' : 's' : _ ) = do
dir <- getCurrentDirectory
content <- getDirectoryContents dir
let filteredContent = sort $ filter (\f -> notElem f [".", ".."]) content
return filteredContent
Pour récupérer l'heure système, on utilise le module Data.Time.LocalTime
pour récupérer l'heure et gérer les fuseaux horaires.
On récupère, le fuseau horaire UTC avec la fonction getCurrentTimeZone
et la date universelle du système avec getCurrentTime
. On utilise alors la fonction utcToLocalTime
. pour obtenir le fuseau horaire et la fonction localTimeOfDay
. pour avoir la date dans un format facilement exploitable.
L'empreinte (TimeOfDay h m p)
permet d'avoir l'heure, les minutes et les secondes de façon indépendante.
Note :Les secondes sont données dans le type Pico
qui contient une partie fractionnaire. Pour obtenir le nombre de secondes sous la forme d'un entier, on utilise simplement la fonction floor
ghci>show p
30.063240062000
ghci>show (floor p)
30
replEval com@(':' : 'h' : 'e' : 'u' : 'r' : 'e' : _ ) = do
tim <- getCurrentTime
zone <- getCurrentTimeZone
let (TimeOfDay h m p) = localTimeOfDay $ utcToLocalTime zone tim
return ["Il est " ++ show h ++ " heures " ++ show m ++ " minutes " ++ show (floor p) ++ " secondes"]
Pour récupérer l'heure système, on utilise le module Data.Time.LocalTime
pour récupérer l'heure et la date du jour et Data.Time.Calendar
pour effectuer des opérations sur les dates.
La date du jour est récupée avec la fonction getCurrentTime
et on utilise utctDay
pour obtenir la date du jour1 et toGregorian
pour traduire la date dans le calendrier Grégorien.
L'empreinte (y, m, d)
permet d'avoir l'année, le mois et les jours de façon indépendante.
replEval com@(':' : 'd' : 'a' : 't' : 'e' : _ ) = do
tim <- getCurrentTime
let (y, m, d) = toGregorian $ utctDay tim
return ["Nous sommes le " ++ show d ++ "/" ++ show m ++ "/" ++ show y]
Différentes informations sur les fichiers peuvent être récupérés telles que la taille en octets avec getFileSize
et la date de modification avec getModificationTime
provenant du module System.Directory
replEval com@(':' : 'i' : 'n' : 'f' : 'o' : ' ' : path) = do
ex <- doesPathExist path
if ex
then do
siz <- getFileSize path
tim <- getModificationTime path
let (y, m, d) = toGregorian $ utctDay tim
return
[ "La taille du fichier " ++ path ++ " est de " ++ show siz ++ " octets"
, "La date de modification du fichier "
++ path
++ " est le "
++ show d
++ "/"
++ show m
++ "/"
++ show y
]
else do
return ["Le fichier " ++ path ++ " n'existe pas"]
Pour quitter le programme, on utilise la fonction exitSuccess
du module System.Exit
qui indique que le programme s'est terminé correctement2.
replEval com@(':' : 'q' : 'u' : 'i' : 't' : 't' : 'e' : 'r' : _ ) = do
putStrLn "Au revoir !"
exitSuccess
return []
Enfin, on traite toutes les autres chaînes de caractère comme étant erronées.
replEval com = return ["Désolé, je ne comprends pas la commande :", " >" ++ com]
L'affichage se fait très simplement avec les fonctions :
putStrLn
:
pour afficher une chaine de caractères à l'écran.
unlines
:
pour convertir une liste de chaînes de caractères en intercalant des sauts de lignes entre les chaînes.
replPrint res = do
putStrLn $ unlines res
La boucle se lance via une fonction qui lance les différentes étapes de la boucle avant de s'appeler récursivement elle-même.
main = do
putStrLn $ unlines help
replLoop
replLoop = do
com <- replRead
res <- replEval com
replPrint res
replLoop
Il y a cependant, un petit problème! L'invite ne s'affiche pas correctement. Lorsque l'on lance la boucle, l'invite ne s'affiche pas et c'est seulement après tapé une commande que l'invite s'affiche.
Cela vient du fait que suivant la configuration du terminal, celui-ci peut créer un tampon (buffer) pour la sortie des commandes. Pour mettre fin à ce comportement, il est possible de désactiver le tampon grâce à la fonction hSetBuffering
du module System.IO
que l'on placera soit en début de programme soit juste avant l'affichage de l'invite.
replRead :: IO String
replRead = do
hSetBuffering stdout NoBuffering
putStr "ma commande>"
com <- getLine
putStrLn com
return com
Et maintenant tout fonctionne bien!
Nous venons de réaliser une boucle REPL très simple mais très limitée. L'interface ne contient pas les fonctionnalités qui permettent de rendre une ligne de commande pratique à utiliser telles que :
L'historique des commandes.
La complétion automatique.
Ces manques peuvent être palliés grâce à la bibliothèque Haskeline
qui permet d'apporter ces fonctionnalités. Nous verrons dans le Tutoriel REPL Haskeline comment l'utiliser.
Notes
1.↑ |
Pour récupérer la date du système, on peut également utiliser la fonction On aura alors:
|
2.↑ |
Finir un programme en indiquant correctement la façon dont il se termine peut-être très utile. |